// 版权归Go作者所有。保留所有权利。
// 此源代码的使用受BSD样式
// 许可证的约束，该许可证可以在许可证文件中找到。

// 清除免费页面。
// 
// 此文件实现清除堆中空闲和未使用的页面（释放支持映射
// 内存的物理页面），以处理页面级
// 碎片，并减少Go应用程序的RSS。
// 
// Go中的清除发生在两个方面：背景
// （异步）清除程序和堆增长（同步）清除程序。
// 
// 前者发生在goroutine上，与背景清扫器非常相似，后者是
// 根据
// 清除成本的数量级估计，软限制在使用突变子时间的清除百分比。背景
// 清道夫的主要目标是将
// 应用程序的估计堆RSS降低到一个目标。
// 
// 该目标定义为：
// （retainExtraPercent+100）/100*（heapGoal/lastHeapGoal）*last_heap_使用
// 
// 本质上，我们希望应用程序的RSS跟踪堆目标，但
// 堆目标是根据对象的字节定义的，而不是像
// RSS这样的页面。因此，我们需要考虑
// /跨度内部的碎片。heapGoal/lastHeapGoal定义当前堆目标
// 和上一个堆目标之间的比率，它告诉我们堆的增长和收缩程度。我们通过采用
// 这个比率并乘以最后一次GC结束时的heap_inuse，来估计堆将以页面的形式增长到什么程度，
// 允许我们考虑这个额外的碎片。注意，这个
// 过程假设碎片的程度在下一个GC循环中不会发生显著变化。高估
// 碎片的数量只会导致更高的内存使用率，这将在下一次调整更新时被计入
// 中。但是，低估碎片可能会导致性能下降。处理本案不在
// 清道夫范围内。在单个GC循环过程中，碎片气球
// 的数量应视为病理，
// 标记为错误，并适当修复。chuang jian defg
// 
// 目标在每次GC和清道夫的起搏参数
// （位于mheap_____）更新匹配后更新。起搏参数的工作原理与背景扫描参数类似。这些参数定义了一条线，其
// 横轴是时间，纵轴是估计的堆RSS，并且
// 清道夫试图始终保持在该线以下。
// 
// 对于堆增长的某些定义，每当堆以
// 大小增长时，就会发生同步堆增长清除。这背后的直觉是，应用程序必须增加堆，因为现有的片段不够大，无法满足页面级内存分配，所以我们迫切地清除这些片段，以抵消RSS的增长。

package runtime

import (
	"internal/goos"
	"runtime/internal/atomic"
	"runtime/internal/sys"
	"unsafe"
)

const (
	// 背景清除器根据这些参数进行调整。
	// 
	// 清除百分比代表我们愿意花费在清除上的突变时间的百分比。
	scavengePercent = 1 // 1%

	// retainExtraPercent表示堆目标
	// 上的内存量，清道夫应将其保留为分配器的缓冲空间。
	// 
	// 维护此开销的目的是拥有更大的
	// 可分配的未屏蔽内存池（因为使用清除的内存
	// 会产生额外成本），以考虑堆碎片和
	// 不断变化的堆布局。
	retainExtraPercent = 10

	// maxPagesPerPhysPage是基于maxPhysPageSize的每个
	// 物理页面支持的最大运行时页面数。
	maxPagesPerPhysPage = maxPhysPageSize / pageSize

	// 清道夫是使用以前的
	// 清除内存和清除内存的成本之间的近似比率。
	// 
	// 对于大多数系统，清除的成本远远超过与使用清除的内存相关的成本，使该常数为0。在其他系统
	// （尤其是那些“sysUsed”不仅仅是禁止操作的系统）上，这一成本是不小的。
	// 
	// 这个比率被用作乘法因子的一部分，以帮助清道夫计算
	// 在其速度调整中使用清除内存的额外成本。
	scavengeCostRatio = 0.7 * (goos.IsDarwin + goos.IsIos)

	// 清道夫保留碎片决定清道夫
	// 一次应该为清道夫保留的内存量。具体地说，
	// 保留的内存量为（以字节为单位的堆大小）/清除服务碎片。
	scavengeReservationShards = 64
)

// Heapretated返回当前堆RSS的估计值。
func heapRetained() uint64 {
	return memstats.heap_sys.load() - atomic.Load64(&memstats.heap_released)
}

// gcpacescanverger更新清道夫的步调，尤其是
// 其速率和RSS目标。为此，它需要当前的heapGoal、
// 和上一个GC循环的heapGoal。
// 
// RSS目标基于当前堆目标，具有较小的开销
// 以适应分配器中的不确定性。
// 
// 起搏基于扫频数据，适用于常规和
// 大页面。更多信息请参见该常数。
// 
// 必须在更新GC速度时调用。
// 
// mheap。锁必须被锁住，否则世界必须停止。
func gcPaceScavenger(heapGoal, lastHeapGoal uint64) {
	assertWorldStoppedOrLockHeld(&mheap_.lock)

	// 如果我们在第一次GC完成之前被调用，请禁用清除功能。
	// 无论如何，我们在第二个GC循环之前都不会进行清理（我们还没有足够的关于堆的
	// 信息），所以这很好，并且避免了错误
	// 或以后的垃圾数据。
	if lastHeapGoal == 0 {
		atomic.Store64(&mheap_.scavengeGoal, ^uint64(0))
		return
	}
	// 计算我们的清除目标。
	goalRatio := float64(heapGoal) / float64(lastHeapGoal)
	retainedGoal := uint64(float64(memstats.last_heap_inuse) * goalRatio)
	// 将RetainExtracent开销添加到retainedGoal。这个计算看起来很奇怪，但目的是得到一个整数除法（例如，如果retainExtraPercent=12.5，那么我们得到的除数是8）
	// ，这也避免了乘法的溢出。
	retainedGoal += retainedGoal / (1.0 / (retainExtraPercent / 100.0))
	// 将其与物理页面边界对齐，以使以下计算更加精确。
	retainedGoal = (retainedGoal + uint64(physPageSize) - 1) &^ (uint64(physPageSize) - 1)

	// 表示我们现在在堆中对RSS的贡献（字节）。
	// 
	// 在
	// PHYSPACESIZE<=页面大小的系统上，保证始终是PHYSPACESIZE的倍数，因为我们以大于
	// 的速率映射heap_sys，并以PHYSPACESIZE的倍数释放内存。
	// 
	// 然而，某些函数将heap_sys重新归类为其他统计数据（例如，
	// stack_sys），这是以页面大小的倍数发生的，因此在系统
	// physPageSize>pageSize中，下面的计算将不准确。
	// 一般来说，这是可以的，因为我们最多会有一个常规的
	// 物理页面。
	retainedNow := heapRetained()

	// 如果我们已经低于目标，或者距离目标不到一页，请禁用背景清道夫。如果
	// 要做的工作少于一页，我们将禁用背景清道夫，因为这不值得。
	if retainedNow <= retainedGoal || retainedNow-retainedGoal < uint64(physPageSize) {
		atomic.Store64(&mheap_.scavengeGoal, ^uint64(0))
		return
	}
	atomic.Store64(&mheap_.scavengeGoal, retainedGoal)
}

// 后台清道夫的睡眠/等待状态。
var scavenge struct {
	lock                 mutex
	g                    *g
	parked               bool
	timer                *timer
	sysmonWake           uint32 // 原子设置。
	printControllerReset bool   // 清道夫是否处于冷却状态。
}

// readyForScavenger向sysmon发送信号，让其唤醒清道夫，因为可能还有新的工作要做。
// 
// 此函数在运行
// 和清除程序被踢醒之间可能存在显著延迟，但在直接调用WakeCleaver不安全的上下文中，可以安全地调用
// 此函数。
func readyForScavenger() {
	atomic.Store(&scavenge.sysmonWake, 1)
}

// 如有必要，唤醒清道夫立即解除清道夫的连接。
// 
// 可以在没有P的情况下运行，但它可以分配，因此在任何分配路径上都不能称为
// 。
// 
// mheap。锁，扫。锁，还有时间表。锁不能被锁住。
func wakeScavenger() {
	lock(&scavenge.lock)
	if scavenge.parked {
		// 通知sysmon它不应该费心叫醒清道夫。
		atomic.Store(&scavenge.sysmonWake, 0)

		// 尝试停止计时，但我们并不在乎是否成功。
		// 可能是计时器从未启动，或者是
		// 我们正在与它赛跑。
		// 在我们与之比赛的情况下，
		// 我们经历了拾荒者的虚假唤醒，但这是绝对安全的。
		stopTimer(scavenge.timer)

		// 解开goroutine，告诉它可能有节奏
		// 改变。请注意，我们跳过调度程序的runnext插槽，因为我们想避免清道夫干扰用户goroutines的公平调度。实际上，这将
		// 清道夫安排在一个“较低优先级”上，但这没关系，因为当它确实被安排时，它将赶上它错过的工作。
		scavenge.parked = false

		// 通过注射来准备goroutine。我们使用injectglist代替ready或goready的
		// 以允许我们在不使用P的情况下运行此函数
		// injectglist还避免将goroutine放置在
		// 当前P的runnext插槽中，这是为了防止
		// 清道夫过多地干扰用户goroutine调度
		// 。
		var list gList
		list.push(scavenge.g)
		injectglist(&list)
	}
	unlock(&scavenge.lock)
}

// screevesleep试图让清道夫睡眠一段时间。
// 
// 请注意，此函数只应由清道夫调用。
// 
// 清道夫可能会因起搏改变而提前醒来，如果起搏改变待定，清道夫可能根本不会入睡。返回实际睡眠时间。
func scavengeSleep(ns int64) int64 {
	lock(&scavenge.lock)

	// 设置计时器。wen jian defg
	start := nanotime()
	resetTimer(scavenge.timer, start+ns)

	// 把自己标记为睡着了，然后去睡觉。
	scavenge.parked = true
	goparkunlock(&scavenge.lock, waitReasonSleep, traceEvGoSleep, 2)

	// 返回我们实际睡眠的时间。
	return nanotime() - start
}

// 背景清道夫。
// 
// 后台清道夫在
// 下面维护应用程序的RSS，该行由
// mheap结构中的比例清道夫统计信息描述。
func bgscavenge(c chan int) {
	scavenge.g = getg()

	lockInit(&scavenge.lock, lockRankScavenge)
	lock(&scavenge.lock)
	scavenge.parked = true

	scavenge.timer = new(timer)
	scavenge.timer.f = func(_ any, _ uintptr) {
		wakeScavenger()
	}

	c <- 1
	goparkunlock(&scavenge.lock, waitReasonGCScavengeWait, traceEvGoBlock, 1)

	// idealFraction是我们花费在清理上的全部应用程序CPU时间的理想百分比。
	idealFraction := float64(scavengePercent) / 100.0

	// 输入：所用CPU时间的分数。
	// 设定值：理想分数。
	// 输出：临界时间与睡眠时间的比率（决定睡眠时间）。
	// 
	// 这个控制器的输出与我们实际想要达到的目标有些间接：睡眠时间。定义
	// 的原因是为了确保控制器的输出与
	// 其输入有直接关系（与反向关系相反），从而使其在某种程度上更易于出于调整目的进行推理。
	critSleepController := piController{
		// 通过齐格勒-尼科尔斯过程进行松散调整。
		kp: 0.3375,
		ti: 3.2e6,
		tt: 1e9, // 1秒重置时间。

		// 这些范围似乎很宽，但我们希望给控制器足够的空间来寻找最佳值。
		min: 0.001,  // 1:1000 
		max: 1000.0, // 1000:1 
	}
	// 我们从什么值开始并不重要，但我们不能为零，因为这会导致被零除的问题。选择一些保守的，我们将
	// 也用作后备方案。
	const startingCritSleepRatio = 0.001
	critSleepRatio := startingCritSleepRatio
	// 我们避免使用控制器的剩余时间（纳秒），
	// 我们将critSleepRatio保持在保守值。如果控制器的
	// 假设无法成立，则使用。
	controllerCooldown := int64(0)
	for {
		released := uintptr(0)
		crit := float64(0)

		// 至少需要1毫秒的清除时间，否则相应的
		// 保持我们所需利用率的睡眠时间太低，
		// 不可靠。
		const minCritTime = 1e6
		for crit < minCritTime {
			// 如果背景清除被禁用，或者如果没有工作要做，就停下来。
			retained, goal := heapRetained(), atomic.Load64(&mheap_.scavengeGoal)
			if retained <= goal {
				break
			}

			// 清道夫是我们试图一次性清除的内存量。较小的值意味着在抢占等情况下，清除器对调度程序的响应更快。较大的值意味着
			// 清除的间接费用可以更好地摊销，因此
			// 清除吞吐量更好。
			// 
			// 选择当前值时，假设每个物理页面的成本约为10µs（这有点悲观），这意味着最坏情况下的延迟为
			// 4个KiB物理页面的延迟约为160µs。当前值偏向于延迟大于吞吐量。
			const scavengeQuantum = 64 << 10

			// 累积清除时间。
			start := nanotime()
			r := mheap_.pages.scavenge(scavengeQuantum)
			atomic.Xadduintptr(&mheap_.pages.scav.released, r)
			end := nanotime()

			// 在某些平台上，如果清除
			// 内存所需的时间小于其时钟（例如Windows）或
			// 由于时钟错误。
			// 
			// 在这种情况下，只需假设清除每个常规物理页面需要10µs 
			// （根据经验确定），保守地忽略大页面
			// 对时间的影响。
			const approxCritNSPerPhysicalPage = 10e3
			if end <= start {
				crit += approxCritNSPerPhysicalPage * float64(r/physPageSize)
			} else {
				crit += float64(end - start)
			}
			released += r

			// 当使用假时间时，只需做一个循环。
			if faketime != 0 {
				break
			}
		}

		if released == 0 {
			lock(&scavenge.lock)
			scavenge.parked = true
			goparkunlock(&scavenge.lock, waitReasonGCScavengeWait, traceEvGoBlock, 1)
			continue
		}

		if released < physPageSize {
			// 如果发生这种情况，这意味着我们可能试图发布物理页面的部分
			// 但其可能的影响是，它发布了整个物理页面，其中一些可能仍在使用。
			// 这可能会导致内存损坏。投
			throw("released less than one physical page of memory")
		}

		if crit < minCritTime {
			// 这意味着没有足够的工作来填满时间。
			// 没关系；我们不应该试图对这些信息做任何事情，因为这会导致一个足够短的睡眠请求，事情会变得一团糟。假设我们至少做了这么多工作。
			// 这一切意味着我们会睡得更长。
			crit = minCritTime
		}

		// 将关键时间乘以1+使用
		// 清除内存与清除内存的成本之比。这迫使我们通过长时间的睡眠和更少的清理来支付急切地重新使用这些内存的成本。更具体地说，我们避免了这样的情况：由于使用被清除的内存会产生额外的开销，我们经常会进行清除，从而影响分配性能。
		crit *= 1 + scavengeCostRatio

		// 根据我们花在工作上的时间来睡觉。
		slept := scavengeSleep(int64(crit / critSleepRatio))

		// 如果我们正在从控制器冷却，请停在这里。crit和sleep并不是时间的精确度量，但是这里有点邋遢也没关系。我们只是希望避免一些短暂的不良行为。
		if controllerCooldown > 0 {
			t := slept + int64(crit)
			if t > controllerCooldown {
				controllerCooldown = 0
			} else {
				controllerCooldown -= t
			}
			continue
		}

		// 计算所花费的CPU时间。
		// 
		// 这对于GOMAXPROCS来说可能有点不准确，但我们是
		// 相对于GOMAXPROCS在一般情况下的变化
		// （它只在世界停止时发生变化，而不是在GC期间发生变化），
		// 噪音中存在着微小的不准确。
		cpuFraction := float64(crit) / ((float64(slept) + crit) * float64(gomaxprocs))

		// 更新critSleepRatio，进行调整，直到达到理想分数。
		var ok bool
		critSleepRatio, ok = critSleepController.next(cpuFraction, idealFraction, float64(slept)+crit)
		if !ok {
			// 控制器的核心假设是，我们可以得到一个成比例的
			// 响应。这可能是暂时的，所以暂时切换到
			// 保持固定的保守睡眠量。
			critSleepRatio = startingCritSleepRatio
			controllerCooldown = 5e9 // 5秒。

			// 向scav跟踪打印机发送信号以输出此信息。
			lock(&scavenge.lock)
			scavenge.printControllerReset = true
			unlock(&scavenge.lock)
		}
	}
}

// 清道夫清道夫清除N字节的免费页面，首先从
// 最高地址开始。连续的调用从它关闭
// 的位置继续，直到堆耗尽。调用ScaveneStartGen将其返回到堆的顶部。
// 
// 返回以字节为单位清除的内存量。
func (p *pageAlloc) scavenge(nbytes uintptr) uintptr {
	var (
		addrs addrRange
		gen   uint32
	)
	released := uintptr(0)
	for released < nbytes {
		if addrs.size() == 0 {
			if addrs, gen = p.scavengeReserve(); addrs.size() == 0 {
				break
			}
		}
		systemstack(func() {
			r, a := p.scavengeOne(addrs, nbytes-released)
			released += r
			addrs = a
		})
	}
	// 只保留未被清理或搜索的空间
	// 以确保我们始终取得进步。
	p.scavengeUnreserve(addrs, gen)
	return released
}

// printScavTrace将扫掠跟踪线打印到标准错误。
// 
// released应该是自上次调用此
// 以来释放的内存量，forced表示是否由
// 应用程序强制执行清除。
// 
// 清除。锁必须锁好。
func printScavTrace(gen uint32, released uintptr, forced bool) {
	assertLockHeld(&scavenge.lock)

	printlock()
	print("scav ", gen, " ",
		released>>10, " KiB work, ",
		atomic.Load64(&memstats.heap_released)>>10, " KiB total, ",
		(atomic.Load64(&memstats.heap_inuse)*100)/heapRetained(), "% util",
	)
	if forced {
		print(" (forced)")
	} else if scavenge.printControllerReset {
		print(" [controller reset]")
		scavenge.printControllerReset = false
	}
	println()
	printunlock()
}

// 清道夫启动新一代清道夫，将清道夫的搜索空间重置为完全使用的地址空间。
// 
// p.mheapLock必须持有。
// 
// 必须在系统堆栈上运行，因为必须持有p.mheapLock。
// 
// go:systemstack 
func (p *pageAlloc) scavengeStartGen() {
	assertLockHeld(p.mheapLock)

	lock(&p.scav.lock)
	if debug.scavtrace > 0 {
		printScavTrace(p.scav.gen, atomic.Loaduintptr(&p.scav.released), false)
	}
	p.inUse.cloneInto(&p.scav.inUse)

	// 为清道夫循环选择新的起始地址。
	var startAddr offAddr
	if p.scav.scavLWM.lessThan(p.scav.freeHWM) {
		// 免费的高水位线超过了“清除的”低水位线，
		// 因此在地址空间的部分
		// 中有免费的可清除页面，清道夫已经搜索过了，高水位线是最高的。选择这一点作为我们的新起点，以确保我们看到这些页面。
		startAddr = p.scav.freeHWM
	} else {
		// 自由的高水位线不超过清除的低水位线
		// 水位线。这意味着分配器没有释放
		// 我们上一个周期清除的范围内的任何内存，所以我们还是继续从我们所在的位置清除
		// 为好。
		startAddr = p.scav.scavLWM
	}
	p.scav.inUse.removeGreaterEqual(startAddr.addr())

	// 如果p.inUse，则reservationBytes可能为零。totalBytes很小，或者如果
	// 清除服务碎片很大。这种情况很好，因为清洗器
	// 将被简单地关闭，但这确实意味着清洗器保留碎片
	// 与pallocChunkBytes一起指示清洗器触发的最小堆大小。实际上，这个最小值通常小于
	// 竞技场的大小，因此几乎每个堆上都有清道夫。
	p.scav.reservationBytes = alignUp(p.inUse.totalBytes, pallocChunkBytes) / scavengeReservationShards
	p.scav.gen++
	atomic.Storeuintptr(&p.scav.released, 0)
	p.scav.freeHWM = minOffAddr
	p.scav.scavLWM = maxOffAddr
	unlock(&p.scav.lock)
}

// 清除服务保留一个连续的地址空间范围
// 用于清除。它保留的最大空间量与堆的大小成正比。范围是从高位地址
// 首先保留的。
// 
// 返回保留范围及其清除生成号。
func (p *pageAlloc) scavengeReserve() (addrRange, uint32) {
	lock(&p.scav.lock)
	gen := p.scav.gen

	// 从保留最小值开始。
	r := p.scav.inUse.removeLast(p.scav.reservationBytes)

	// 如果大小为零，请提前返回；我们不想使用
	// 下面的假地址。
	if r.size() == 0 {
		unlock(&p.scav.lock)
		return r, gen
	}

	// 清除剂要求碱基与
	// palloc块对齐，因为这是
	// 清除剂的操作单元，所以向下对齐，可能会扩展
	// 范围。
	newBase := alignDown(r.base.addr(), pallocChunkBytes)

	// 无论我们刚刚拿出多少额外的钱，都要从inUse中删除。
	p.scav.inUse.removeGreaterEqual(newBase)
	unlock(&p.scav.lock)

	r.base = offAddr{newBase}
	return r, gen
}

// 清道夫恢复返回先前由清道夫服务保留的范围的未复仇部分。
func (p *pageAlloc) scavengeUnreserve(r addrRange, gen uint32) {
	if r.size() == 0 {
		return
	}
	if r.base.addr()%pallocChunkBytes != 0 {
		throw("unreserving unaligned region")
	}
	lock(&p.scav.lock)
	if gen == p.scav.gen {
		p.scav.inUse.add(r)
	}
	unlock(&p.scav.lock)
}

// 清除一个地址范围的工作，直到它找到一个连续运行的页面来清除。它将尝试一次最多清除最大字节数
// 但可能会清除更多字节，以避免
// 破坏巨大的页面。一旦它清除了一些内存，它返回
// 以字节为单位清除了多少内存。返回已清除的字节数和尚未搜索的工作部分。
// 
// 作品的基址必须与pallocChunkBytes对齐。
// 
// 必须在系统堆栈上运行，因为它获取了p.mheapLock。
// 
// go:systemstack 
func (p *pageAlloc) scavengeOne(work addrRange, max uintptr) (uintptr, addrRange) {
	// 防御检查我们是否收到了空地址范围。
	// 如果是，请返回。
	if work.size() == 0 {
		// 无事可做。
		return 0, work
	}
	// 检查工作的先决条件。
	if work.base.addr()%pallocChunkBytes != 0 {
		throw("scavengeOne called with unaligned work region")
	}
	// 计算要清除的最大页数。
	// 
	// 这应该是对齐的（max，pageSize）/pageSize，但是max可以并且将会是
	// uintptpr（0），所以我们需要非常小心，不要在这里溢出。
	// 首先计算向下取整的页数，而不是使用alignUp，如果需要，再加上一页。
	maxPages := max / pageSize
	if max%pageSize != 0 {
		maxPages++
	}

	// 计算我们可以清除的最小页数。
	// 
	// 因为我们只能清除整个物理页面，所以我们必须确保每次至少清除minPages，将
	// 与minPages*pageSize对齐。
	minPages := physPageSize / pageSize
	if minPages < 1 {
		minPages = 1
	}

	// 快速路径：检查包含工作中最高地址的区块。
	if r, w := p.scavengeOneFast(work, minPages, maxPages); r != 0 {
		return r, w
	} else {
		work = w
	}

	// findCandidate乐观地找到下一个正在工作的清道夫候选人。
	// 
	// 返回候选块索引，成功时返回true，失败时返回false。
	// 
	// 堆不需要锁定。
	findCandidate := func(work addrRange) (chunkIdx, bool) {
		// 重复这部作品的各个部分。
		for i := chunkIndex(work.limit.addr() - 1); i >= chunkIndex(work.base.addr()); i-- {
			// 如果这个区块完全在使用中，或者没有未屏蔽的页面，不要麻烦
			// 进行更复杂的检查。
			// 
			// 注意，我们在访问摘要和块时没有锁定，但是
			// 没关系。无论如何，我们都很乐观。

			// 快速检查是否有足够的免费页面。
			if p.summary[len(p.summary)-1][i].max() < uint(minPages) {
				continue
			}

			// 仔细检查这块区域，寻找更难的候选人。再说一遍，我们可以和很多不同的代码竞争，但我们只是乐观。但是，请确保以原子方式加载l2指针，以避免堆增长带来的竞争。在这种情况下，如果我们确实与堆增长竞争，可能也可能不可能看到nil指针，但只需防御性地忽略nil即可。无论如何，这次行动是乐观的。
			l2 := (*[1 << pallocChunksL2Bits]pallocData)(atomic.Loadp(unsafe.Pointer(&p.chunks[i.l1()])))
			if l2 != nil && l2[i.l2()].hasScavengeCandidate(minPages) {
				return i, true
			}
		}
		return 0, false
	}

	// 慢路径：在正在使用的地址空间中乐观地迭代
	// 寻找任何免费且未屏蔽的页面。如果我们认为自己看到了什么，请锁定并验证它！
	for work.size() != 0 {

		// 搜索候选人。
		candidateChunkIdx, ok := findCandidate(work)
		if !ok {
			// 我们没有找到候选人，所以我们结束了。
			work.limit = work.base
			break
		}

		// 锁，这样我们就可以验证我们发现了什么。
		lock(p.mheapLock)

		// 如果可以的话，找到、验证并清除。
		chunk := p.chunkOf(candidateChunkIdx)
		base, npages := chunk.findScavengeCandidate(pallocChunkPages-1, minPages, maxPages)
		if npages > 0 {
			work.limit = offAddr{p.scavengeRangeLocked(candidateChunkIdx, base, npages)}
			unlock(p.mheapLock)
			return uintptr(npages) * pageSize, work
		}
		unlock(p.mheapLock)

		// 我们被愚弄了，所以让我们从我们结束的地方继续。
		work.limit = offAddr{chunkBase(candidateChunkIdx)}
	}
	return 0, work
}

// ScreeveOneFast是ScreeveOne的快速路径，它只检查顶部的
// 。必须在系统堆栈上运行，因为它获取了堆锁。
// 
// go:systemstack 
func (p *pageAlloc) scavengeOneFast(work addrRange, minPages, maxPages uintptr) (uintptr, addrRange) {
	maxAddr := work.limit.addr() - 1
	maxChunk := chunkIndex(maxAddr)

	lock(p.mheapLock)
	if p.summary[len(p.summary)-1][maxChunk].max() >= uint(minPages) {
		// 我们只会在至少有
		// minPages免费页面的情况下费心寻找候选人。
		base, npages := p.chunkOf(maxChunk).findScavengeCandidate(chunkPageIndex(maxAddr), minPages, maxPages)

		// 如果我们发现了什么东西，就把它清理干净，然后回来！
		if npages != 0 {
			work.limit = offAddr{p.scavengeRangeLocked(maxChunk, base, npages)}
			unlock(p.mheapLock)
			return uintptr(npages) * pageSize, work
		}
	}
	unlock(p.mheapLock)

	// 更新限制以反映我们已经检查了maxChunk的事实。
	work.limit = offAddr{chunkBase(maxChunk)}
	return 0, work
}

// 清道夫锁定清除给定的内存区域。
// 内存区域由其块索引（ci）描述，
// 该区域相对于该
// 块（基）的起始页索引，以及该区域的页长（npages）。返回被清除区域的基址。
// 
// p.mheapLock必须保持。解锁p.mheapLock，但在返回前重新获取
// 它。因此必须在systemstack上运行。
// 
// go:systemstack 
func (p *pageAlloc) scavengeRangeLocked(ci chunkIdx, base, npages uint) uintptr {
	assertLockHeld(p.mheapLock)

	// 计算范围开头的完整地址。
	addr := chunkBase(ci) + uintptr(base)*pageSize

	// 将要清除的范围标记为已分配，因为
	// 在清除过程中，我们不希望任何正在分配的goroutines抓取它。
	if scav := p.allocRange(addr, uintptr(npages)); scav != 0 {
		throw("double scavenge")
	}

	// 完成后，就可以安全解锁了。
	unlock(p.mheapLock)

	// 更新清除低水位线。
	lock(&p.scav.lock)
	if oAddr := (offAddr{addr}); oAddr.lessThan(p.scav.scavLWM) {
		p.scav.scavLWM = oAddr
	}
	unlock(&p.scav.lock)

	if !p.test {
		// 只有在不进行测试的情况下，才能执行实际的清除操作。
		// 否则这样做很危险。
		sysUnused(unsafe.Pointer(addr), uintptr(npages)*pageSize)

		// 仅当不在测试中时更新全局记帐，否则
		// 运行时的记帐将错误。
		nbytes := int64(npages) * pageSize
		atomic.Xadd64(&memstats.heap_released, nbytes)

		// 也更新一致的会计。
		stats := memstats.heapStats.acquire()
		atomic.Xaddint64(&stats.committed, -nbytes)
		atomic.Xaddint64(&stats.released, nbytes)
		memstats.heapStats.release()
	}

	// 重新锁定堆，因为现在我们需要使这些页面
	// 可用分配。将它们释放回页面分配器。
	lock(p.mheapLock)
	p.free(addr, uintptr(npages), true)

	// 将范围标记为已清除。
	p.chunkOf(ci).scavenged.setRange(base, npages)
	return addr
}

// fillAligned返回x，但在m对齐的
// 如果组中的任何位不为零，则将m个位的组设置为1。例如，fillAligned（0x0100a3，8）=0xff00ff。
// 
// 注意，如果m==1，这是不可操作的。
// 
// m必须是2的幂<=maxPagesPerPhysPage。
func fillAligned(x uint64, m uint) uint64 {
	apply := func(x uint64, c uint64) uint64 {
		// 这里使用的技术源自
		// https:
		// 并通过使用适当的常数扩展到不仅仅是字节（如半字节
		// 和uint16s）。
		// 
		// 总结一下这项技术，引用该页面的话：“
		// /”[It]首先将[8]的高位归零
		// 字中的字节。随后，它添加了一个数字，即如果
		// 将导致字节高位溢出。接下来，用这些值对原始字的高位
		// 最初设置了任何低位，
		// 位进行OR运算；
		// 因此，如果设置了
		// 字节中的任何位，则设置字节的高位。最后，我们通过对除
		// 位中是否有一位为零。“
		// 高位之外的所有高位进行OR运算并反转结果，来确定这些高位
		return ^((((x & c) + c) | x) | c)
	}
	// 将x变换为在每个m对齐的
	// 一组m零位的顶部包含1位。
	switch m {
	case 1:
		return x
	case 2:
		x = apply(x, 0x5555555555555555)
	case 4:
		x = apply(x, 0x7777777777777777)
	case 8:
		x = apply(x, 0x7f7f7f7f7f7f7f7f)
	case 16:
		x = apply(x, 0x7fff7fff7fff7fff)
	case 32:
		x = apply(x, 0x7fffffff7fffffff)
	case 64: // /==maxPagesPerPhysPage 
		x = apply(x, 0x7fffffffffffffff)
	default:
		throw("bad m value")
	}
	// 现在，x中每个m对齐的组的顶部位都被设置为
	// 该组在原始x中均为零。

	// 从每组m位减去1。
	// 使用我们只知道设置了每个
	// m对齐组的顶部位，我们知道这将
	// 设置每个组的所有位，除了
	// 顶部位，所以只需或与原始
	// 结果一起设置所有位。
	return ^((x - (x >> (m - 1))) | x)
}

// hasScavengeCandidate如果在这个
// min页的空闲和未屏蔽内存，则返回true。
// pallocData表示的区域中有任何最小页对齐的
// 
// min必须是2的非零幂<=maxPagesPerPhysPage。
func (m *pallocData) hasScavengeCandidate(min uintptr) bool {
	if min&(min-1) != 0 || min == 0 {
		print("runtime: min = ", min, "\n")
		throw("min must be a non-zero power of 2")
	} else if min > maxPagesPerPhysPage {
		print("runtime: min = ", min, "\n")
		throw("min too large")
	}

	// 此搜索的目标是查看该块是否包含任何可用的未屏蔽内存。ABCFDG 
	for i := len(m.scavenged) - 1; i >= 0; i-- {
		// 函数，因为从技术上讲，我们只需要
		// 计算的前半部分就可以了。它将节省一些指令
		// 但会增加一些额外的代码复杂性。
		x := fillAligned(m.scavenged[i]|m.pallocBits[i], uint(min))

		// 快速跳过非免费或已清除的页面块。
		if x != ^uint64(0) {
			return true
		}
	}
	return false
}

// findScavengeCandidate返回此pallocData 
// 段的开始索引和大小，该段表示可用和未屏蔽内存的连续区域。
// 
// searchIdx指示此区块内的页面索引以开始搜索，但请注意，findScavengeCandidate通过pallocData向后搜索。作为
// 结果，它将按地址顺序返回最高的清除候选地址。
// 
// min表示页面运行的最小硬大小和对齐方式。也就是说，
// FindScavengCandidate将不会返回小于最小页面大小的区域，
// 或是最小页面大小或更大但未与最小页面对齐的区域。最小区域必须是
// 2的非零次幂<=maxPagesPerPhysPage。
// 
// max是一个暗示，表明一个地区需要多大。如果max>=pallocChunkPages，那么
// findScavengeCandidate将有效地返回整个自由和未受攻击的区域。
// 如果max<pallochunkpages，它可能会截断返回的区域，使其大小为
// max。但是，如果对于
// 示例，它选择保留巨大的页面，或者如果max没有与min对齐（它将取整），findScavengeCandidate仍然可能返回更大的区域。也就是说，即使max很小，返回的大小也不能保证
// 等于max。max允许小于min，在这种情况下，就好像
// max==min。
func (m *pallocData) findScavengeCandidate(searchIdx uint, min, max uintptr) (uint, uint) {
	if min&(min-1) != 0 || min == 0 {
		print("runtime: min = ", min, "\n")
		throw("min must be a non-zero power of 2")
	} else if min > maxPagesPerPhysPage {
		print("runtime: min = ", min, "\n")
		throw("min too large")
	}
	// max可能没有最小对齐，所以我们可能会意外地截断到
	// 一个max值，从而导致我们返回一个未对齐的值。
	// 为防止出现这种情况，请将最大值与最小值的倍数对齐（始终为
	// 2的幂）。这也防止了max永远小于
	// min，除非它为零，所以要明确地处理它。
	if max == 0 {
		max = min
	} else {
		max = alignUp(max, min)
	}

	i := int(searchIdx / 64)
	// 从快速跳过非空闲或已清除页面块开始。
	for ; i >= 0; i-- {
		// 1被清除或非自由=>0未被清除且自由
		x := fillAligned(m.scavenged[i]|m.pallocBits[i], uint(min))
		if x != ^uint64(0) {
			break
		}
	}
	if i < 0 {
		// 找不到任何自由/未被清除的页面。
		return 0, 0
	}
	// 我们在i的64位块中有一些东西，但它可以进一步扩展。循环直到我们找到它的范围。

	// 1s被清除或非自由=>0s未被清除和自由
	x := fillAligned(m.scavenged[i]|m.pallocBits[i], uint(min))
	z1 := uint(sys.LeadingZeros64(^x))
	run, end := uint(0), uint(i)*64+(64-z1)
	if x<<z1 != 0 {
		// 在移出z1位后，我们仍然有1s，
		// 因此运行在这个字内结束。
		run = uint(sys.LeadingZeros64(x << z1))
	} else {
		// 在移出z1位后，我们没有更多的1。
		// 这意味着运行延伸到
		// 单词的底部，因此可能会延伸到更多单词。
		run = 64 - z1
		for j := i - 1; j >= 0; j-- {
			x := fillAligned(m.scavenged[j]|m.pallocBits[j], uint(min))
			run += uint(sys.LeadingZeros64(x))
			if x != 0 {
				// 在这个词中，跑步停止了。
				break
			}
		}
	}

	// 如果长度大于最大值，则拆分我们找到的跑步记录，但请保留原始长度，因为我们以后可能需要它。
	size := run
	if size > uint(max) {
		size = uint(max)
	}
	start := end - size

	// 每个巨大的页面都保证可以放入一个palloc块中。
	// 
	// TODO（mknyszek）：支持更大的页面大小。ABCFDG
	if physHugePageSize > pageSize && physHugePageSize > physPageSize {
		// 我们有巨大的页面，所以让我们确保不会通过在巨大的页面边界上清除
		// 来破坏一个页面。如果射程[start，start+size]与
		// 一个免费且未经修补的巨大页面重叠，我们希望扩大我们清理的区域
		// 以包含该巨大页面。

		// 计算候选页面上方的巨大页面边界。
		pagesPerHugePage := uintptr(physHugePageSize / pageSize)
		hugePageAbove := uint(alignUp(uintptr(start), pagesPerHugePage))

		// 如果该边界在我们当前的候选页面内，那么我们可能正在打破
		// 一个巨大页面。
		if hugePageAbove <= end {
			// 计算候选人下方的巨大页面边界。
			hugePageBelow := uint(alignDown(uintptr(start), pagesPerHugePage))

			if hugePageBelow >= end-run {
				// 由于开始+大小跨越了
				// 一个巨大的页面边界，并将开始四舍五入到最近的巨大
				// 页面边界包含在我们发现的完整运行中，因此我们面临着拆分一个巨大页面的危险。将整个
				// 大页面四舍五入到大页面大小中。
				size = size + (start - hugePageBelow)
				start = hugePageBelow
			}
		}
	}
	return start, size
}
